#coding: UTF-8

require 'socket'
require 'timeout'
require './client.rb'


# MESSAGE
OK_CONNECT = 100 
OK_QUIT = 101
OK_AFK = 102
OK_NFK = 103
OK_LIST_AFK = 104
OK_LIST_NFK = 105
OK_LIST_PRIVATE = 106
OK_LIST_ASK_PRIVATE = 107
OK_NICK = 108
OK_TALK = 109
OK_ACCEPT = 110
OK_REJECT = 111
OK_CLOSE_TALK = 112
OK_MSG = 113
OK_CLOSE = 114
OK_ASK_SEND = 115 
OK_ACCEPT_FILE = 116
OK_REJECT_FILE = 117 
OK_SEND = 118
HELP_OK = 119
OK_DATE = 120
OK_PONG = 121

# MESSAGE ERROR :
ERR_CONF_TALK_DENIED = 400
ERR_NO_SUCH_NICK = 401
ERR_NO_SEND_REQUEST = 403
ERR_UNKNOWN_COMMAND = 404
ERR_NO_NICK_NAME_GIVEN = 405
ERR_ERRONEUS_NICKNAME = 406
ERR_NICKNAME_IN_USE = 407
ERR_NICK_EGAL_PRESENT = 408
ERR_NEED_MORE_PARAMS = 409
ERR_NO_TALK_REQUEST = 410
ERR_NO_CURRENT_TALK = 411
ERR_UNAVAILABLE_FILE = 412
ERR_NICKNAME_FALSE = 413
ERR_TALK_EXIST = 414

# Signaux
CONNECTED = 501
DISCONNECTED = 502 
AWAY = 503
NICKNAME = 504
ASK_TALK = 505
ACCEPTED_TALK = 506
REFUSED_TALK = 507
MESSAGE_PUBLIC = 508
MESSAGE_PRIVATE = 509
CLOSE = 510
ASK_TRANSFERT = 511
ACCEPT_FILE = 512
REFUSED_FILE = 513
END_TALK = 514
ACTIVE = 515
FAIL_SEND = 516
RECEIVE = 517
PING = 518


class Serveur

  # Fonction de base d'un seveur
  def initialize(tcp_Server)
    @sockServeur = tcp_Server
    
    # Ouverture en ecriture du fichier log
    @log = File.open("#{$0}.log", "w")
    
    # liste des membre connecte
    @connected = []
    
    # semaphore
    @sem_log = Mutex.new()
  end # initialize
  
  def close
    @sockServeur.close
    log("Stop serveur")
    @log.close
  end # close


  def run
    log("Start serveur")
    
    loop do
      add_connected(@sockServeur.accept)
    end # loop
  end # run
  
  
  def add_connected(sockClient)
      Thread.new do
        begin
          sockClient.puts("Welcome in our Junk Irc serveur, the most junky protocol to talk with yours friends!")
          etat_connexion(sockClient)
          
        rescue Exception => e
          client = @connected.find{|a| a.tcp == sockClient}
          quit_client(client, "client disconnect") if client
        ensure
          sockClient.close
        end # begin
      end # Thread.new
  end # add_connected



  #outils

  # Utile uniquement si on ne tient pas compte de la case, sinon il faut utiliser la méthode value? des Array
  def pseudo_exist?(pseudo)
    pseudo_maj = pseudo.upcase
    
    @connected.each{|e| return true if e.pseudo.upcase == pseudo_maj}
    
    return false
  end # pseudo_exist?


  # Utile uniquement si on ne tient pas compte de la case, sinon il faut utiliser la méthode value? des Array
  def pseudo_nfk_exist?(pseudo)
    pseudo_maj = pseudo.upcase

    @connected.each{|e| return e.nfk if e.pseudo.upcase == pseudo_maj}

    return false
  end # pseudo_exist?

  def pseudo_valid?(pseudo)
    pseudo_maj = pseudo.upcase
    return (pseudo.length <= 20 and pseudo_maj != "ADMIN" and pseudo_maj != "PUBLIC")
  end # pseudo_valid?


  #convertit un pseudo en client
  def pseudo_to_client(pseudo)
    pseudo_maj = pseudo.upcase

    @connected.each{|e| return e if e.pseudo.upcase == pseudo_maj}
    
    return nil
  end # pseudo_to_client


  # supprime les différents engagement du client
  def quit_client(client, message)
    if client

      client.talk.each do |e|
        client.delete_talk(e)

        e.delete_talk(client)
        send_message_to(e, "#{END_TALK} #{client}")
      end # each
      
      client.request_talk.each do |e|
        client.delete_request_talk(e)
        e.delete_request_talk(client)
        
        send_message_to(e, "#{REFUSED_TALK} #{client}")
      end # each
      
      client.ask_file.each do |e|
        e.each do |f|
          client.delete_ask_send(client, f)
          client.delete_ask_send(e, f)
          
          send_message_to(e, "#{REFUSED_FILE} #{e} #{f}")
        end # each
      end # each
      
      @connected.delete(client)
      
      client_connect = @connected.find_all{|e| e}
      
      if message.empty?
        log("#{client} Disconnected")
        client_connect.each{|e| e.puts("#{DISCONNECTED} #{client}")}
      else
        log("#{client} Disconnected #{message}")
        client_connect.each{|e| e.puts("#{DISCONNECTED} #{client} #{message}")}
      end
    end # if
    
  end # quit_client


  # message serveur / client
  def log(msg)
    @sem_log.lock
      message = "[#{Time.now}] #{msg}"
      
      @log.puts message
      puts message
    @sem_log.unlock
  end # log

  def send_message_all(message)
    fork do # on fork car l'envoie d'un message à tout le monde peux prendre du temps
      @connected.each do |e|
        begin
          send_message_to(e, message) if e.nfk
        end # begin
      end # each
    end # fork
  end # send_message_all

  # Sécurise l'envoi d'un message à un autre client. Envoie true si le message est parvenu à destination
  def send_message_to(client, message)
    client.puts(message)
    
    return true
    
  rescue # si le client a fermé la connexion
    quit_client(client, "client disconnect")
    
    return false
  end


  # time out
  def time_out(client)
    client.puts(PING)
    
    requete = timeout(5){client.tcp.gets.chomp}
    
    # for the rfc...
    if(requete.upcase == "/PONG")
      client.puts(OK_PONG)
    end # if
      
    afk(client)
    etat_attente(client)
    etat_transmission(client)
    
  rescue RuntimeError => e
    quit_client(client, "Time out")
    exit 0
  end





  #commande

  def unknown(client)
    log("Recive an incorect command from #{client}") 
    client.puts(ERR_UNKNOWN_COMMAND)
  end # unknown


  def connect(soket, pseudo)
    
    if pseudo_exist?(pseudo)
      soket.puts ERR_NICKNAME_IN_USE
      return nil
    elsif not pseudo_valid?(pseudo)
      soket.puts ERR_NICKNAME_FALSE
      return nil
    elsif pseudo.include?("/")
      soket.puts ERR_ERRONEUS_NICKNAME
      return nil
    else
      client = Client.new(soket, pseudo)
      @connected << client
      
      log("#{pseudo} Connected")
      client.puts(OK_CONNECT)
      send_message_all("#{CONNECTED} #{pseudo}")
      
      return client
    end # if
  end # connect

  def quit(client, message)
    quit_client(client, message)
    
    client.puts(OK_QUIT)
  end # quit


  # message

  def msg(client, pseudo, message)
    
    if pseudo.upcase == "PUBLIC"
      send_message_all("#{MESSAGE_PUBLIC} #{client} #{message}")
      client.puts(OK_MSG)
      
      log("#{client} to #{pseudo}: #{message}")
    elsif pseudo_nfk_exist?(pseudo) 
      
      other_client = pseudo_to_client(pseudo)
      
      if client.talk?(other_client)
        if send_message_to(other_client, "#{MESSAGE_PRIVATE} #{client} #{message}")
          client.puts(OK_MSG)
          
          log("#{client} to #{pseudo}: #{message}")
        else
          client.puts(ERR_NO_SUCH_NICK)
        end # if
      else
        client.puts(ERR_CONF_TALK_DENIED)
      end # if
    else
      client.puts(ERR_NO_SUCH_NICK)
    end # if
  end # msg


  # Near / away from keyboard

  def afk(client)
    client.nfk = false
    
    
      client.talk.each do |e|
        client.delete_talk(e)
        e.delete_talk(client)
        
        send_message_to(e, "#{END_TALK} #{client}")
      end # each
      
      client.request_talk.each do |e|
        client.delete_request_talk(e)
        e.delete_request_talk(client)
        
        send_message_to(e, "#{REFUSED_TALK} #{client}")
      end # each
      
      client.ask_file.each do |e|
        e.each do |f|
          client.delete_ask_send(client, f)
          client.delete_ask_send(e, f)
          
          send_message_to(e, "#{REFUSED_FILE} #{e} #{f}")
        end # each
      end # each
      

    client.puts(OK_AFK)
    send_message_all("#{AWAY} #{client}")
    log("#{client} is away from keyboard. All his ask to send a file or to talk are cancel")
    
    etat_attente(client)
  end # afk

  def nfk(client)
    client.nfk = true
    
    log("#{client} is near from keyboard")
    send_message_all("#{ACTIVE} #{client}")
    
    client.puts(OK_NFK)
  end # afk




  # Talk

  def talk(client, pseudo)
    if not pseudo_nfk_exist?(pseudo)
      client.puts(ERR_NO_SUCH_NICK)
    elsif client.pseudo.upcase == pseudo.upcase
      client.puts(ERR_NICK_EGAL_PRESENT)
    else
      other_client = pseudo_to_client(pseudo)
      
      if client.talk?(other_client)
        client.puts(ERR_TALK_EXIST)
        
      # On regarde si une requète n'a pas déjà était envoyé dans l'autre sens
      elsif client.request_talk?(other_client)
      
      
        if send_message_to(other_client, "#{ACCEPTED_TALK} #{client}")
          #on supprime les demandes
          client.delete_request_talk(other_client)
          
          # on conserve le lien entre ces 2 client
          other_client.add_talk(client)
          client.add_talk(other_client)
          
          #on confirme aux deux
          client.puts OK_TALK
          client.puts "#{ACCEPTED_TALK} #{other_client}"
          
          log("#{client} and #{other_client} have a private conversation")
        else
          client.puts(ERR_NO_SUCH_NICK)
        end # if
      else
        if send_message_to(other_client, "#{ASK_TALK} #{client}")
          other_client.add_request_talk(client)
          client.puts OK_TALK
          log("#{client} ask to talk at #{other_client}")
        else
          client.puts(ERR_NO_SUCH_NICK)
        end # if
      end # if
    end # if

  end # talk


  def accept(client, pseudo)
    if not pseudo_nfk_exist?(pseudo)
      client.puts(ERR_NO_SUCH_NICK)
    elsif client.pseudo.upcase == pseudo.upcase
      client.puts(ERR_NICK_EGAL_PRESENT)
    else
      other_client = pseudo_to_client(pseudo)
      
      if client.request_talk?(other_client)
        if send_message_to(other_client, "#{ACCEPTED_TALK} #{client}")
          #on supprime la demande
          client.delete_request_talk(other_client)
          
          # on conserve le lien entre ces 2 clients
          other_client.add_talk(client)
          client.add_talk(other_client)
          
          #on confirme aux deux
          client.puts OK_ACCEPT
          log("#{client} and #{other_client} have a private conversation")
        else
          client.puts(ERR_NO_SUCH_NICK)
        end # if
      else
        client.puts ERR_NO_TALK_REQUEST
      end #if
    end # if
  end # accept

  def reject(client, pseudo)
    if not pseudo_nfk_exist?(pseudo)
      client.puts(ERR_NO_SUCH_NICK)
    elsif client.pseudo.upcase == pseudo.upcase
      client.puts(ERR_NICK_EGAL_PRESENT)
    else
      other_client = pseudo_to_client(pseudo)
      if client.request_talk?(other_client)
        if send_message_to(other_client, "#{REFUSED_TALK} #{client}")
          #on supprime la demande
          client.delete_request_talk(other_client)
          
          # On confirme aux deux clients
          client.puts OK_REJECT
          log("#{client} refuse the #{other_client}'s ask to a private conversation")
        else
          client.puts(ERR_NO_SUCH_NICK)
        end # if
      else
        client.puts ERR_NO_TALK_REQUEST
      end #if
    end # if
  end # accept


  def close_talk(client, pseudo)
    if not pseudo_nfk_exist?(pseudo)
      client.puts(ERR_NO_SUCH_NICK)
    else
      other_client = pseudo_to_client(pseudo)
      
      if not client.talk?(other_client)
        client.puts(ERR_NO_CURRENT_TALK)
      else
        if send_message_to(other_client, "#{END_TALK} #{client}")
          client.delete_talk(other_client)
          other_client.delete_talk(client)
        
          client.puts(OK_CLOSE_TALK)
          log("#{client} close his talk with #{other_client}")
        else
          client.puts(ERR_NO_SUCH_NICK)
        end # if
      end # if
    end # if
  end # close




  # List

  def list_afk(client)
    list = @connected.find_all{|e| not e.nfk }.join(" ")
    log("Demande de la liste des utilisateurs afk de #{client}")
    
    client.puts("#{OK_LIST_AFK} #{list}")
  end # list_afk

  def list_nfk(client)
    list = @connected.find_all{|e| e.nfk }.sort.join(" ")
    log("Demande de la liste des utilisateurs nfk de #{client}")
    
    client.puts("#{OK_LIST_NFK} #{list}")
  end # list_nfk

  def list_private(client)
    list = client.talk.sort.join(" ")
    
    log("Demande de la liste des demande privée de #{client}")
    
    client.puts("#{OK_LIST_ASK_PRIVATE} #{list}")
  end # list_private

  def list_ask_private(client)
    list = client.request_talk.sort.join(" ")
    
    log("Demande de la liste des demande privée de #{client}")
    
    client.puts("#{OK_LIST_ASK_PRIVATE} #{list}")
  end # list_ask_private




  #File

  def ask_send(client, pseudo, file_path, size)
    if not pseudo_nfk_exist?(pseudo)
      client.puts(ERR_NO_SUCH_NICK)
    elsif client.pseudo.upcase == pseudo.upcase
      client.puts(ERR_NICK_EGAL_PRESENT)
    else
      name_file = file_path.split("/").last
      if name_file.split(".").last == "txt"
        client.puts(ERR_UNAVAILABLE_FILE)
      else
        other_client = pseudo_to_client(pseudo)
        if send_message_to(other_client, "#{ASK_TRANSFERT} #{client} #{name_file} #{size}")
          other_client.add_ask_send(client, name_file)
          client.puts OK_ASK_SEND
          log("#{client} ask to send at #{other_client} the file #{name_file} (#{size}")
        else
          client.puts(ERR_NO_SUCH_NICK)
        end # if
      end # if
    end # if
  end # ASK_SEND


  def reject_file(client, pseudo, name_file)
    if not pseudo_nfk_exist?(pseudo)
      client.puts(ERR_NO_SUCH_NICK)
    elsif client.pseudo.upcase == pseudo.upcase
      client.puts(ERR_NICK_EGAL_PRESENT)
    else
      other_client = pseudo_to_client(pseudo)
      
      if client.ask_send?(other_client, name_file)
        if send_message_to(other_client, "#{REFUSED_FILE} #{client} #{name_file}")
          #on supprime la demande
          client.delete_ask_send(other_client, name_file)
          
          # On confirme aux deux clients
          client.puts OK_REJECT
          log("#{client} refuse the #{other_client}'s ask to send the file #{name_file}")
        else
          client.puts(ERR_NO_SUCH_NICK)
        end # if
      else
        client.puts ERR_NO_SEND_REQUEST
      end #if
    end # if
  end # reject_file


  def accept_file(client, pseudo, name_file, ip, port)
    if not pseudo_nfk_exist?(pseudo)
      client.puts(ERR_NO_SUCH_NICK)
    elsif client.pseudo.upcase == pseudo.upcase
      client.puts(ERR_NICK_EGAL_PRESENT)
    else
      other_client = pseudo_to_client(pseudo)

      
      if client.ask_send?(other_client, name_file)
        if send_message_to(other_client, "#{ACCEPT_FILE} #{client} #{name_file} #{ip} #{port}")
        
          # On supprime la demande
          client.delete_ask_send(other_client, name_file)
          
          # On confirme
          client.puts OK_ACCEPT
          
          log("#{client} accept the #{other_client}'s ask to send the file #{name_file}")
        else
          client.puts(ERR_NO_SUCH_NICK)
        end # if
      else
        client.puts ERR_NO_SEND_REQUEST
      end #if
    end # if
  end # reject_file






  # Other

  def nick(client, pseudo)
    if pseudo_exist?(pseudo)
      client.puts ERR_NICKNAME_IN_USE
    elsif not pseudo_valid?(pseudo)
      client.puts ERR_NICKNAME_FALSE
    elsif pseudo.include?("/")
      client.puts ERR_ERRONEUS_NICKNAME
    else
      old_pseudo = client.pseudo
      client.pseudo = pseudo
      
      client.puts(OK_NICK)
      send_message_all("#{NICKNAME} #{old_pseudo} #{client}")
      log("#{old_pseudo} is now know as #{client}")
    end # if
  end # nick

  def date(client)
    time = Time.new
    log("#{client} request time")
    client.puts("#{OK_DATE} #{time.strftime("%H:%M:%S-%d/%m/%Y")}")
  end # date





  # Etats

  def etat_connexion(tcp)
    loop do
      requete = tcp.gets.chomp
      commande, pseudo, *autre = requete.split
      
      if autre.length == 0 and commande.upcase == "/CONNECT" and pseudo
        client = connect(tcp, pseudo)
        etat_transmission(client) if client
      elsif not pseudo and commande.upcase == "/HELP"
        help_etat_connexion(tcp)
      else
        unknown(tcp)
      end #if
    end # loop
  
  end # traiterClient

  def etat_transmission(client)
    loop do
      requete = timeout(300){client.tcp.gets.chomp}
      
      commande = requete.split()[0]
      
      case commande.upcase
        when "/MSG"
          commande, pseudo, *tab_message = requete.split()
          message = tab_message.join(" ")
          
          msg(client, pseudo, message)
        when "/QUIT"
          commande, *tab = requete.split()
          message = tab.join(" ")
          
          quit(client, message)
          break;
        when "/LIST_AFK"
          list_afk(client)
        when "/LIST_NFK"
          list_nfk(client)
        when "/LIST_ASK_PRIVATE"
          list_ask_private(client)
        when "/LIST_PRIVATE"
          list_private(client)
        when "/AFK"
          afk(client)
        when "/NFK"
          nfk(client)
        when "/NICK"
          commande, pseudo, *tab = requete.split()
          if tab.length == 0 and pseudo
            nick(client, pseudo)
          else
            unknown(client)
          end # if
        when "/TALK"
          commande, pseudo, *tab = requete.split()
          if tab.length == 0 and pseudo
            talk(client, pseudo)
          else
            unknown(client)
          end # if
        when "/ACCEPT"
          commande, pseudo, *tab = requete.split()
          if tab.length == 0 and pseudo
            accept(client, pseudo)
          else
            unknown(client)
          end # if
        when "/REJECT"
          commande, pseudo, *tab = requete.split()
          if tab.length == 0 and pseudo
            reject(client, pseudo)
          else
            unknown(client)
          end # if
        when "/CLOSE_TALK"
          commande, pseudo, *tab = requete.split()
          if tab.length == 0 and pseudo
            close_talk(client, pseudo)
          else
            unknown(client)
          end # if
        when "/ASK_SEND"
          commande, pseudo, file, size, *tab = requete.split()
          if tab.length == 0 and pseudo and file and client.talk?(pseudo_to_client(pseudo)) # Que si on parle avec l'autre pseudo
            ask_send(client, pseudo, file, size)
          else
            unknown(client)
          end # if     
        when "/ACCEPT_FILE"
          commande, pseudo, file, ip, port, *tab = requete.split()
          if tab.length == 0 and pseudo and file and client.talk?(pseudo_to_client(pseudo)) # Que si on parle avec l'autre pseudo
            accept_file(client, pseudo, file, ip, port)
          else
            unknown(client)
          end # if  
        when "/REJECT_FILE"
          commande, pseudo, file, *tab = requete.split()
          if tab.length == 0 and pseudo and file and client.talk?(pseudo_to_client(pseudo)) # Que si on parle avec l'autre pseudo
            reject_file(client, pseudo, file)
          else
            unknown(client)
          end # if   
        when "/DATE"
          date(client)
        when "/HELP"
          help_etat_transmission(client)
        else
          unknown(client)
      end # case
      
    end # loop
    
    etat_connexion(client.tcp)
    
  rescue RuntimeError => e
    time_out(client)
  end # traiterClient


  def etat_attente(client)
    commande = ""
    
    loop do
      requete = client.tcp.gets.chomp
      commande, *autre = requete.split
      
      if commande.upcase == "/NFK" and autre.length == 0
          nfk(client)
          break
      elsif commande.upcase == "/QUIT"
          commande, *tab = requete.split()
          message = tab.join(" ")
          
          quit(client, message)
          break
      elsif commande.upcase == "/HELP"
          help_etat_attente(client)
      else
        unknown(client)
      end # case
    end # loop
    
    if(commande.upcase == "/QUIT")
      etat_connexion(client.tcp)
    else
      etat_transmission(client)
    end
  end # traiterClient





  # help

  def help_etat_connexion(client)
    log("An unknown client ask help")
    client.puts("#{HELP_OK} /CONNECT")
  end # help_etat_connexion
  
  def help_etat_transmission(client)
    log("#{client} client ask help")
    client.puts("#{HELP_OK} /QUIT, /AFK, /LIST_NFK, /LIST_AFK, /LIST_PRIVATE, /LIST_ASK_PRIVATE, /NICK, /TALK, /ACCEPT, /REJECT, /CLOSE_TALK, /MSG, /CLOSE, /ASK_SEND, /ACCEPT_FILE, /REJECT_FILE, /SEND")
  end # help_etat_connexion
  
  def help_etat_attente(client)
    log("#{client} client ask help")
    client.puts("#{HELP_OK} /QUIT, /NFK")
  end # help_etat_connexion

end # Serveur










begin
  raise "Usage : serveur_JIP [listened ip}" if (ARGV.length > 1)
  ARGV[0] = '' if (ARGV.length != 1)


  
  sockServeur = TCPServer.new(ARGV[0], 8080)
  
  # serveur
  serveur = Serveur.new(sockServeur)
  serveur.run
  
    
  # Mise en place d'un gestionnaire pour capturer SIGINT
  trap("SIGINT") do
    serveur.stop
  end # trap
  

  
  rescue Exception => e
  STDERR.puts e
end # begin


